package com.nulldev.util.java.scripting.nscript;

import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

import com.nulldev.util.java.JVMUtil;
import com.nulldev.util.java.scripting.nscript.nScriptSecurity.Permissions;
import com.nulldev.util.java.scripting.nscript.nScriptSecurity.ScriptSecurityException;
import com.nulldev.util.string.StringsUtil;
import com.nulldev.util.uri.URIUtil;
import com.nulldev.util.uri.URIUtil2;

@Deprecated
/**
 * WORK IN PROGRESS
 * @author null
 *
 */
public class nScript {
	private boolean DEBUG = false;
    
    private String script;
    private ArrayList<nScriptInstruction> instructions;
    private int index;
    private nScriptListener listener;
    private ArrayList<nScriptVariable> variables;
    private PrintStream outStream;
    private int currentLine;

    private String scriptName;
    private long permissions;
    //private ScriptSecurity.Modes securityMode;
    
    public nScript(final String script) {
        this(script, getTenFirstChars(script));
    }
    
    public nScript(String script, String name) {
        //this.securityMode = Modes.ONLY_BLOCK_CRITICAL;
        this.script = script.replaceAll(Pattern.quote(";"), "\n");
        this.scriptName = name;
        this.outStream = System.out;
        this.variables = new ArrayList<nScriptVariable>();
        this.grantPermissions(Permissions.GET_SYSTEM, Permissions.ALTER_CLASSPATH);
        
        this.parseScript(script);
    }
    
    private static String getTenFirstChars(final String s) {
        int maxIndex = 10;
        if (maxIndex >= s.length()) {
            maxIndex = s.length() - 1;
        }

        return s.substring(0, maxIndex);
    }
    
    public nScript setName(final String n) {
        this.scriptName = n;
        return this;
    }
    
    public String getEntireScript() {
        return this.script;
    }
    
    public void setListener(final nScriptListener l) {
        this.listener = l;
    }
    
    private void parseScript(final String s) {
        String[] lines = s.split("\r|\n");
        this.instructions = new ArrayList<nScriptInstruction>(); 
        int lineNbr = 0;
        for (String line : lines) {
            if (line.isEmpty()) {
                continue;
            }
            this.currentLine++;
            while (line.charAt(0) == ' ' || line.charAt(0) == '\t') {
                line = line.substring(1);
            }
            
            String parts[] = line.split(" ");
            String instruction = parts[0];
            String[] args = new String[0];
            if (this.doesInstructionHasArgs(instruction)) {
                args = splitCorrectly(parts, 1, parts.length);
            }
            this.instructions.add(new nScriptInstruction(instruction, args, lineNbr++));
        }
    }
    
    private String[] splitCorrectly(final String[] parts, final int i, final int length) {
        if (parts.length >= length && i <= parts.length && i >= 0) {
            ;
        }else {
            throw new nScriptParseException("Invalid index/length");
        }
        ArrayList<String> list = new ArrayList<String>();
        String toSplit = new String();
        for (int index = i; index < length; index++) {
            toSplit += (index - i == 0 ? "" : " ") + parts[index];
        }
        if (toSplit.trim().isEmpty()) {
            return new String[0];
        }
        if (DEBUG) {
        	System.err.println("Received: " + toSplit);
        }
        
        boolean inDoubleQuote = false;
        boolean inSingleQuote = false;
        char previous = '\0';
        String current = new String();
        for(int index = 0; index < toSplit.length(); index++) {
            char c = toSplit.charAt(index);
            if(c == '\r' || c == '\n') {
                if(inDoubleQuote) {
                    throw new nScriptParseException("Unbalanced quote at line: " + this.currentLine);
                }else if(inSingleQuote) {
                    throw new nScriptParseException("Unbalanced quote at line: " + this.currentLine);
                }
            }
            if(previous == '\\') {
                if(c == '\\') {
                    current+='\\';
                }else if(c == '"') {
                    current+='"';
                }
            }else {
                if(c == '"') {
                    inDoubleQuote = !inDoubleQuote;
                }else if(c == '\'') {
                    inSingleQuote = !inSingleQuote;
                }else if(c == ' ') {
                    if(inDoubleQuote || inSingleQuote) {
                        current+=" ";
                    }else {
                        list.add(current);
                        current = new String();
                        previous = '\0';
                    }
                }else {
                    current+=c;
                }
            }

            previous = c;
        }
        if (inDoubleQuote) {
            throw new nScriptParseException("Unbalanced quote at line: " + this.getInfo(this.currentLine));
        }else if(inSingleQuote) {
            throw new nScriptParseException("Unbalanced quote at line: " + this.getInfo(this.currentLine));
        }
        list.add(current);
        return list.toArray(new String[0]);
    }
    
    private String getInfo(final int currentLine) {
        return "(at " + scriptName + ",line:" + currentLine + ")";
    }
    
    private boolean doesInstructionHasArgs(final String instruction) {
        if (instruction.equals("exit")) {
            return false;
        }
        return true;
    }
    
    public void injectIntoClasspath(final URL path) {
        try {
            URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
            Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", new Class<?>[] { 
            																	URL.class }); 
            addURL.setAccessible(true);
            addURL.invoke(classLoader, new Object[]{ path });
            System.out.println("Injected " + path);
        }
        catch(Exception e) {
            e.printStackTrace();
        }
    }
    
    public static final int NO_EXECUTION = 0;
    public static final int TRUE = 2;
    public static final int FALSE = 1;
    public static final int VOID = 10000;
    public static final int EXIT_EXECUTION = -1;
    
    private final HashMap<String, nInstructionExecuted> _instructions = new HashMap<String, nInstructionExecuted>();
    
    private String output = "";
    
    public <T> void println(final T o) {
    	output += o + "\n";
    	this.outStream.println(o);
    }
    
    private void checkDefaultInstructions() {
    	if (_instructions.isEmpty()) {
    		_instructions.put("nscript", new nInstructionExecuted() {
				
				@Override
				public int instruction(nScriptInstruction inst, nScript from) throws nScriptParseException, ScriptSecurityException {
					if (!from.hasPermission(Permissions.GET_SYSTEM)) {
						throw new ScriptSecurityException("nScript: Cannot read system variables due to permissions!");
					}
					final String[] args = inst.getArgs();
					if (StringsUtil.isEqualCM(args, 0, "asObject()")) {
						from.println("nScript[version=1.0,date=Date[02,08,2018]]");
					}else {
						from.println("nScript v1.0");
					}
					return VOID;
				}
			});
    		_instructions.put("web.pingable", new nInstructionExecuted() {
				
				@Override
				public int instruction(nScriptInstruction inst, nScript from)
						throws nScriptParseException, ScriptSecurityException {
					if (!from.hasPermission(Permissions.INTERNET)) {
						throw new ScriptSecurityException("nScript: Cannot access the internet because of permissions!");
					}
					if (inst.getArgs().length == 0) {
						from.println("pingable [url]");
						return VOID;
					}
					try {
						final boolean r = URIUtil2.ProxyUtils.accessible(URIUtil.safeCreate(inst.getArgs()[0])).getA();
						from.println(r);
						return (r ? TRUE : FALSE);
					} catch (Exception e) {
						throw new nScriptParseException(e);
					}
				}
			});
    		_instructions.put("web.granted", new nInstructionExecuted() {
				
				@Override
				public int instruction(nScriptInstruction inst, nScript from)
						throws nScriptParseException, ScriptSecurityException {
					from.println(from.hasPermission(Permissions.INTERNET));
					return from.hasPermission(Permissions.INTERNET) ? TRUE : FALSE;
				}
			});
    	}
    	return;
    }
    
    private int executeInstruction(final nScriptInstruction in) throws nScriptParseException {
    	checkDefaultInstructions();
    	if (this.listener != null) {
    		this.listener.onInstruction(in);
    	}
    	try {
    		final String instruction = in.getInstruction();
    		final String[] args = in.getArgs();
    		if (instruction == null || instruction.equals("")) {
    			return NO_EXECUTION;
    		}
    		if (instruction.equals("exit")) {
    			return EXIT_EXECUTION;
    		}
    		for(int i = 0;i < args.length; i++) {
                if (args[i].startsWith("$") && getVarByName(args[i].replace("$", "")) != nScriptVariable.NULL_VAR) {
                    args[i] = getVarByName(args[i].replace("$", "")).getValue();
                }else {
                    args[i] = format(args[i]);
                }
            }
    		for (final Map.Entry<String, nInstructionExecuted> inst : ((HashMap<String, nInstructionExecuted>) this._instructions.clone()).entrySet()) {
    			if (inst.getKey().equals(instruction)) {
    				return inst.getValue().instruction(in, nScript.this);
    			}
    		}
    	}catch (Throwable t) {
    		throw new nScriptParseException(t);
    	}
    	return NO_EXECUTION;
    }
    
    private String getArg(final String[] args, final int i, final int line)
    {
        return this.getArg(args, i, line, "" + i);
    }
    
    private String getArg(final String[] args, final int i, final int line, final String name) {
        if(i >= args.length) {
            throw new nScriptParseException("Missing argument '" + name + "' at index " + i + " of line " + line);
        }
        return args[i];
    }
    
    private nScriptVariable getVarByName(final String name) {
        for (nScriptVariable var : this.variables) {
            if (name.equals(var.getName())) {
                return var;
            }
        }
        return nScriptVariable.NULL_VAR;
    }
    
    private String format(String string) {
        if (listener != null) {
            string = listener.formatArg(string);
        }
        return string;
    }

	public void run() {
		while (this.hasNext()) {
			this.executeNext();
		}
	}
	
	public boolean hasNext() {
        if (index == -1) {
            return false;
        }
        return index < instructions.size();
    }

    public void executeNext() {
        if (hasNext()) {
            if (this.executeInstruction(instructions.get(index)) != -1) {
                index++;
            }else {
                index = -1;
            }
        }
    }
    
    public final void grantPermission(final Permissions perm) {
        this.permissions |= 1 << perm.ordinal();
    }
    
    public final boolean hasPermission(final Permissions perm) {
        if ((permissions | (1 << perm.ordinal())) == permissions) {
            return true;
        }
        return false;
    }
    
    public final void grantPermissions(final Permissions... permissions) {
    	for (final Permissions perm : permissions) {
    		this.grantPermission(perm);
    	}
    }

	public String read() {
		return this.output;
	}
}
